using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Linq;

namespace NHibernateInsight.Tests.Insights.Linq.Extensions
{
    /// <summary>
    /// 
    /// http://marcinbudny.blogspot.com/2009/10/typed-expand-for-linq-to-nhiberante.html
    /// 
    /// NOTE: Additionally added a couple of more overloads
    ///  - Expand(string, FetchMode)
    ///  - Expand(selector, FetchMode)
    ///  - Expand(selector, subpath, FetchMode)
    ///
    /// 
    /// Original Copyright:
    /// 
    /// Copyright (c) 2009 Marcin Budny
    /// All rights reserved.
    /// 
    /// Redistribution and use in source and binary forms, with or without modification,
    /// are permitted provided that the following conditions are met:
    /// 
    ///     * Redistributions of source code must retain the above copyright notice,
    ///     this list of conditions and the following disclaimer.
    ///     * Redistributions in binary form must reproduce the above copyright notice,
    ///     this list of conditions and the following disclaimer in the documentation
    ///     and/or other materials provided with the distribution.
    ///     * The name of Marcin Budny may not be used to endorse or promote products derived from this
    ///     software without specific prior written permission.
    /// 
    /// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    /// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    /// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    /// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
    /// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    /// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    /// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    /// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    /// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    /// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    /// </summary>
    public static class NHibernateLinqExpandExtensions
    {
        public static INHibernateQueryable<T> Expand<T>(
            this INHibernateQueryable<T> nhQueryable,
            string path, FetchMode fetchMode)
        {
            nhQueryable.QueryOptions.RegisterCustomAction(c => c.SetFetchMode(path, fetchMode));

            return nhQueryable;
        }
        
        public static INHibernateQueryable<T> Expand<T, K, L>(
            this INHibernateQueryable<T> nhQueryable,
            Expression<Func<T, K>> selector, SubpathBuilder<L> subpaths)
        {
            var mainPath = ProcessSelector(selector);

            foreach (var subPathSelector in subpaths.Expressions)
            {
                var subpath = ProcessSelector(subPathSelector);
                nhQueryable.Expand(mainPath + "." + subpath);
            }

            return nhQueryable;
        }

        public static INHibernateQueryable<T> Expand<T, K>(
            this INHibernateQueryable<T> nhQueryable,
            Expression<Func<T, K>> selector)
        {
            if (selector.Body.NodeType != ExpressionType.MemberAccess)
                throw new ArgumentException("Selector has to be of MemberAccess type", "selector");

            nhQueryable.Expand(ProcessSelector(selector));

            return nhQueryable;
        }

        public static INHibernateQueryable<T> Expand<T, K, L>(
            this INHibernateQueryable<T> nhQueryable,
            Expression<Func<T, K>> selector, SubpathBuilder<L> subpaths, FetchMode fetchMode)
        {
            var mainPath = ProcessSelector(selector);

            foreach (var subPathSelector in subpaths.Expressions)
            {
                var subpath = ProcessSelector(subPathSelector);
                nhQueryable.Expand(mainPath + "." + subpath, fetchMode);
            }

            return nhQueryable;
        }

        public static INHibernateQueryable<T> Expand<T, K>(
            this INHibernateQueryable<T> nhQueryable,
            Expression<Func<T, K>> selector, FetchMode fetchMode)
        {
            if (selector.Body.NodeType != ExpressionType.MemberAccess)
                throw new ArgumentException("Selector has to be of MemberAccess type", "selector");

            nhQueryable.Expand(ProcessSelector(selector), fetchMode);

            return nhQueryable;
        }

        private static string ProcessSelector(LambdaExpression expression)
        {
            var memberStack = new Stack<string>();
            TraverseMemberAccess(expression.Body as MemberExpression, memberStack);

            return JoinMembers(memberStack);
        }

        private static void TraverseMemberAccess(MemberExpression expression,
                                                 Stack<string> memberStack)
        {
            if (expression != null)
            {
                memberStack.Push(expression.Member.Name);

                var methodCall = expression.Expression as MethodCallExpression;
                if (expression.Expression is MethodCallExpression)
                {
                    TraverseIndexerCall(expression.Expression as MethodCallExpression,
                                        memberStack);
                }
                else if (expression.Expression is MemberExpression)
                {
                    TraverseMemberAccess(expression.Expression as MemberExpression,
                                         memberStack);
                }
                else if (!(expression.Expression is ParameterExpression))
                {
                    throw new ArgumentException("Selector contains invalid expression of type"
                                                + expression.Expression.NodeType, "selector");
                }
            }
        }

        private static void TraverseIndexerCall(MethodCallExpression methodCall,
                                                Stack<string> memberStack)
        {
            if (methodCall != null)
            {
                var type = methodCall.Object.Type;

                var interfaces = type.GetInterfaces().Select(i => i.Name).ToList();
                if (type.IsInterface)
                    interfaces.Add(type.Name);

                if ((interfaces.Contains("IList") || interfaces.Contains("IList`1"))
                    && (methodCall.Method.Name == "get_Item"))
                {
                    TraverseMemberAccess(methodCall.Object as MemberExpression,
                                         memberStack);
                }
                else
                {
                    throw new ArgumentException("Selector contains invalid method call",
                                                "selector");
                }
            }
        }

        private static string JoinMembers(Stack<string> memberStack)
        {
            return string.Join(".", memberStack.ToArray());
        }
    }

    public static class Subpath
    {
        public static SubpathBuilder<T> For<T>()
        {
            return new SubpathBuilder<T>();
        }
    }
    public class SubpathBuilder<T>
    {
        public IList<LambdaExpression> Expressions { get; private set; }

        public SubpathBuilder()
        {
            Expressions = new List<LambdaExpression>();
        }

        public SubpathBuilder<T> Add<K>(Expression<Func<T, K>> selector)
        {
            if (selector.Body.NodeType != ExpressionType.MemberAccess)
                throw new ArgumentException("Selector has to be of MemberAccess type", "selector");

            Expressions.Add(selector);
            return this;
        }
    }
}